Skip to content

Forwarding Refs

We saw how the Slider component from earlier was essentially a "supercharged range input". With prop delegation, I can pass any attributes I like to it, and they'll be forwarded onto the <input type="range"> automatically.

But what if I want to focus this slider on mount?

Here's a broken implementation. If you'd like, spend a couple of moments poking at the code, and see what you can learn about what's going on here:

Code Playground

import React from 'react';

import Slider from './Slider';

function App() {
const [volume, setVolume] = React.useState(50);
// Create a React ref:
const sliderRef = React.useRef();
React.useEffect(() => {
// Focus the slider on mount:
sliderRef.current.focus();
}, []);
return (
<main>
<Slider
// Capture a reference to the slider:
ref={sliderRef}
label="Volume"
min={0}
max={100}
value={volume}
onChange={(event) => {
setVolume(event.target.value);
}}
/>
</main>
);
}

export default App;

Something went wrong

/App.js: Cannot read properties of undefined (reading 'focus') (13:22) 10 | 11 | React.useEffect(() => { 12 | // Focus the slider on mount: > 13 | sliderRef.current.focus(); ^ 14 | }, []); 15 | 16 | return (

  1. Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Check the render method of `App`. at Slider (https://sandpack-bundler.vercel.app/Slider.js:30:20) at main at App (https://sandpack-bundler.vercel.app/App.js:24:42)
  2. Cannot read properties of undefined (reading 'focus')
  3. The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  4. The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  5. Could not consume error:
    [[Error]]
  6. Could not consume error:
    [[Error]]

Let's dig into it:

Video Summary

As we learned in the “Running on Mount” lesson, we can focus an input element by following a 3 step process:

  1. Create a container to hold the element with the useRef hook
  2. Capture a reference to the element and store it in the container with the ref attribute
  3. Focus the element on mount, inside an effect

When we repeat these same steps on the sandbox above, however, we don't get an auto-focused range input, we get an error:

Cannot read properties of undefined (reading 'focus')

The key difference in this case is that we're using the ref attribute on a React element that describes a Slider component, instead of on an element that describes a DOM node.

An <input> React element will correspond to a DOM node, but a <Slider> tag has no such relation.

Now, if we look at how Slider is defined, we see that we're using the prop delegation trick. We're essentially creating a “supercharged” range slider, forwarding all props like min, max, value, and onChange.

It's the most intuitive thing in the world to assume that the ref prop will also be forwarded, but unfortunately, it doesn't work that way.

I think it'll be helpful if we de-abstract this code, to understand what's going on at a lower level.

First, let's create a <Slider> React element without JSX:

const sliderElem = React.createElement(
Slider,
{
ref: sliderRef,
label: 'Volume',
min: 0,
}
);

When we run this code, React creates an “element”. If we log it out, we see an object that describes a Slider instance:

console.log(sliderElem);
{
type: ƒ Slider(),
props: {
label: "Volume",
min: 0
},
key: null,
ref: sliderRef,
_owner: {
// Stuff omitted
},
_store: {}
}

A React element is a JavaScript object that describes a particular node in the React tree. When React renders, it'll invoke the Slider component, and pass along all of the props in this object.

This stuff is complicated, and it's easy to get lost in the weeds, but here's the takeaway: ref is a “reserved word” in React, like key. When React creates the element, it plucks out these values, removing them from the props.

In essence, the ref applies to the <Slider> element, and not to the <input> within.

How do we fix it? Well, one option is to come up with our own name for this prop. For example:

// App.js
function App() {
const sliderRef = React.useRef();
return (
<Slider
forwardedRef={sliderRef}
/>
);
}
// Slider.js
function Slider({ label, forwardedRef, ...delegated }) {
return (
<input
ref={forwardedRef}
/>
);
}

Anything other than ref and key, the two reserved prop names, will be passed through as props. So I can come up with whatever name I want, like forwardedRef, to pass the ref through, to capture the <input> element inside Slider.

This used to be how we solved this problem, but it was a bit annoying. We had to remember to use ref with DOM nodes, and forwardedRef with components. And on a large team, there's no guarantee that it'll be consistent; some components might use forwardedRef, others might use delegatedRef, or refPass. You'd have to see how the component is implemented to be sure.

Fortunately, the React team has introduced a better solution: the React.forwardRef helper.

Like React.memo, React.forwardRef is a higher-order component. It's a function that takes a component as an argument, and returns an augmented version of that component.

Here's what it looks like:

import React from 'react';
import styles from './Slider.module.css';
function Slider(
{ label, ...delegated },
ref
) {
const id = React.useId();
return (
<div className={styles.wrapper}>
<label htmlFor={id} className={styles.label}>
{label}
</label>
<input
ref={ref}
{...delegated}
type="range"
id={id}
className={styles.slider}
/>
</div>
);
}
export default React.forwardRef(Slider);

When React renders this component, it supplies a new 2nd argument: in addition to the props object (commonly destructured), we now receive a ref. As the producer of this component, we can choose to apply this ref to whichever DOM node we choose.

From the consumer side, it means we can always use the ref attribute, and things will Just Work:

<Slider ref={sliderRef} />

The ref we apply to the <Slider> element will be forwarded to the Slider component when it's rendered.

Why isn't this the default behaviour?? In earlier versions of React, it was possible to use refs to capture component instances when applied to elements that describe components. We can't do this with function components, and honestly there are very few valid use cases for doing it with class components.

Changing how refs work would be a pretty big breaking change, and it could make it harder to migrate legacy applications. As far as I know, that's the only reason we need to fuss with React.forwardRef.

I have seen some discussions online that this might change in the future. Hopefully, we can skip the React.forwardRef step in the future!

Here's the solution from the video: